package cn.cubix.flow.security;

import cn.cubix.flow.mapper.*;
import cn.cubix.flow.entity.WfPrmsRole;
import cn.cubix.flow.entity.WfPrmsUser;
import cn.cubix.flow.entity.WfUser;
import cn.cubix.flow.service.WfRoleService;
import cn.cubix.flow.service.WfUserService;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor(onConstructor=@__({@Autowired,@Lazy}))
public class SecurityService implements UserDetailsService {

    /** 用户名称 */
    private static final String USERNAME = JWTClaimNames.SUBJECT;
    /** 权限列表 */
    private static final String AUTHORITIES = "aus";

    private final SecurityProperties properties;

    private final WfPrmsMapper wfPrmsMapper;
    private final WfRoleUserMapper wfRoleUserMapper;
    private final WfPrmsRoleMapper wfPrmsRoleMapper;
    private final WfPrmsUserMapper wfPrmsUserMapper;
    private final WfRoleMapper wfRoleMapper;
    private final WfUserService wfUserService;
    private final WfRoleService wfRoleService;
    private final PasswordEncoder passwordEncoder;

    @Autowired@Lazy
    private SecurityService _this;

    public String generateToken(Map<String, Object> claims) {
        String token = null;
        try {
            Date now = new Date();
            JWSSigner signer = new MACSigner(properties.getTokenSecret());
            JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS512)
                    .contentType("JWS")
                    .build();
            JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder();
            claims.forEach(claimsSet::claim);
            claimsSet.issueTime(now);
            claimsSet.expirationTime(new Date(now.getTime()+properties.getTokenExpire()));
            SignedJWT signedJWT = new SignedJWT(header,claimsSet.build());
            signedJWT.sign(signer);
            token = signedJWT.serialize();

            if(properties.isTokenEncrypt()){
                // TODO encrypt
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return token;
    }

    public String generateToken(UserDetails userDetail) {
        Map<String, Object> claims = new HashMap<>(4);
        claims.put(USERNAME, userDetail.getUsername());
        claims.put(AUTHORITIES, userDetail.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
        return _this.generateToken(claims);
    }

    public String getTokenFromRequest(HttpServletRequest request) {
        String token = request.getHeader(properties.getTokenHeader());
        if(StrUtil.isNotEmpty(token) && token.startsWith(properties.getTokenPrefix())){
            token = token.substring(properties.getTokenPrefix().length());
        }else if(request.getCookies()!=null){
            for(Cookie cookie : request.getCookies()){
                if(properties.getTokenHeader().equals(cookie.getName())){
                    token = cookie.getValue();
                    break;
                }
            }
        }
        return token;
    }

    public JWT validateToken(String token){
        JWT result = null;
        try {
            if(StrUtil.isEmpty(token)){
                return null;
            }
            JWT jwt = JWTParser.parse(token);
            if(jwt instanceof EncryptedJWT){
                // TODO decript
            }
            if(jwt instanceof SignedJWT){
                Date now = new Date();
                SignedJWT signedJWT = (SignedJWT) jwt;
                JWSVerifier verifier = new MACVerifier(properties.getTokenSecret());
                if(!signedJWT.verify(verifier)){
                    return null;
                }
                if(now.after(signedJWT.getJWTClaimsSet().getExpirationTime())){
                    return null;
                }
                result = signedJWT;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public UserDetails getUserFromRequest(HttpServletRequest request) {
        String token = _this.getTokenFromRequest(request);
        JWT validatedToken = _this.validateToken(token);
        if(validatedToken!=null){
            try {
                JWTClaimsSet claimsSet = validatedToken.getJWTClaimsSet();
                String username = claimsSet.getStringClaim(USERNAME);
                String authoritieStr = StrUtil.toString(claimsSet.getClaim(AUTHORITIES));
                Collection<? extends GrantedAuthority> authorities
                        = AuthorityUtils.createAuthorityList(StrUtil.isNotEmpty(authoritieStr)?authoritieStr.split(","):new String[0]);
                return new User(username,"",authorities);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 用户名密码登录，通过用户名加载UserDetails
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        WfUser wfUser = wfUserService.getOne(new LambdaQueryWrapper<WfUser>().eq(WfUser::getUsername,username));
        if(wfUser==null){
            log.warn("登录用户[{}]不存在。",username);
            throw new UsernameNotFoundException(String.format("登录用户[%s]不存在",username));
        }
        List<WfPrmsRole> wfPrmsRoleList = wfPrmsRoleMapper.pageByUserId(new Page<>(1,Integer.MAX_VALUE),wfUser.getId(),new LambdaQueryWrapper<>()).getRecords();
        List<WfPrmsUser> wfPrmsUserList = wfPrmsUserMapper.pageByUserId(new Page<>(1,Integer.MAX_VALUE),wfUser.getId(),new LambdaQueryWrapper<>()).getRecords();
        Set<String> wfPrmsCodeSet = new HashSet<>(wfPrmsRoleList.size()+wfPrmsUserList.size());
        wfPrmsRoleList.forEach(wfPrmsRole -> {
            if(Boolean.TRUE.equals(wfPrmsRole.getDisabled())){
                wfPrmsCodeSet.remove(wfPrmsRole.getPrms().getCode());
            }else{
                wfPrmsCodeSet.add(wfPrmsRole.getPrms().getCode());
            }
        });
        wfPrmsUserList.forEach(wfPrmsUser -> {
            if(Boolean.TRUE.equals(wfPrmsUser.getDisabled())){
                wfPrmsCodeSet.remove(wfPrmsUser.getPrms().getCode());
            }else{
                wfPrmsCodeSet.add(wfPrmsUser.getPrms().getCode());
            }
        });
        Set<SimpleGrantedAuthority> authorities = wfPrmsCodeSet.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
        return new User(wfUser.getUsername(),wfUser.getPassword(),authorities);
    }

    /**
     * 密码加密
     * @param rawPassword 明文密码
     * @return 加密密码
     */
    public String encodePassword(String rawPassword) {
        return passwordEncoder.encode(rawPassword);
    }

    /**
     * 密码校验
     * @param rawPassword 明文密码
     * @param encodedPassword 加密密码
     * @return 校验结果
     */
    public boolean checkPassword(CharSequence rawPassword, String encodedPassword) {
        return passwordEncoder.matches(rawPassword,encodedPassword);
    }

}
